/**
 ********************************************************************
 * @author roocket
 * @file    curl.c
 * @version V0.0.0
 * @date    2024/3/22
 * @brief   This file is to indicate curl functions.
 * @attention Code file rules:
 * rule: file encoding use UTF8;
 * rule: max line length 120 characters;
 * rule: line separator \r\n;
 * rule: use clion auto code format tool.
 */
#include <stdio.h>
#include <string.h>
#include <curl/curl.h>
#include <stdbool.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include "curl.h"

void _curl_download_file_start(void *file, const char *location, const void *user, const char *open_way)
{
#if USE_MMAP
    struct curl_user_hdl *cl_user = (struct curl_user_hdl *)user;
    int *fd = (int *)file;
    *fd = open(location, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    if (*fd == -1)
    {
        perror("open user curl_file_location error");
        return;
    }
    if (lseek(*fd, cl_user->file_size, SEEK_SET) == -1)
    {
        perror("lseek error");
        close(*fd);
        return;
    }
    if (write(*fd, "", 1) != 1)
    {
        perror("write error");
        close(*fd);
        return;
    }
    printf("filezise:%d\n", cl_user->file_size);
#else
    FILE **p = (FILE **)file;
    *p = fopen(location, open_way);
    if (NULL == *p)
    {
        perror("open user curl_file_location error");
        return;
    }
#endif
}

void _curl_download_file_stop(void *file)
{
#if USE_MMAP
    int *fd = (int *)file;
    if (*fd)
        close(*fd);
#else
    FILE **p = (FILE **)file;
    if (*p)
        fclose(*p);
    *p = NULL;
#endif
}

static size_t _curl_write_file_data(void *p, size_t size_of, size_t size, void *file)
{
#if USE_MMAP
    struct curl_download_st *dl_st = (struct curl_download_st *)file;
    /*1.copy to buffer 16k*/
    size_t real_size = size_of * size;
    /*write last package to file*/
    if (p == NULL)
    {
        if (dl_st->buffer_left == dl_st->curl_buffer_size)
            return 0;
        int file = dl_st->mmap_file;
        unsigned char *file_ptr = (unsigned char *)mmap(NULL, dl_st->curl_buffer_size, PROT_READ | PROT_WRITE, MAP_SHARED, file, dl_st->curl_buffer_size * dl_st->mmap_id);
        memcpy(file_ptr, dl_st->curl_buffer, dl_st->package_offset);
        munmap(file_ptr, dl_st->curl_buffer_size);
        return 0;
    }
    if (dl_st->buffer_left > 0)
    {
        if (dl_st->buffer_left > real_size)
        {
            // printf("(%s)size:%ld, off:%lld\n", __func__, size_of * size, offset);
            memcpy(dl_st->curl_buffer + dl_st->package_offset, p, real_size);
            dl_st->buffer_left = dl_st->buffer_left - real_size;
            dl_st->package_offset += real_size;
        }
        else
        {
            // printf("(%s)fullsize:%ld, off:%lld\n", __func__, size_of * size, offset);
            memcpy(dl_st->curl_buffer + dl_st->package_offset, p, dl_st->buffer_left);
            dl_st->package_offset = real_size - dl_st->buffer_left;
            p += dl_st->buffer_left;
            dl_st->buffer_left = 0;
        }
    }
    /*2.mmap*/
    if (dl_st->buffer_left == 0)
    {
        int file = dl_st->mmap_file;
        unsigned char *file_ptr = (unsigned char *)mmap(NULL, dl_st->curl_buffer_size, PROT_READ | PROT_WRITE, MAP_SHARED, file, dl_st->curl_buffer_size * dl_st->mmap_id);
        memcpy(file_ptr, dl_st->curl_buffer, dl_st->curl_buffer_size);
        munmap(file_ptr, dl_st->curl_buffer_size);
        dl_st->buffer_left = dl_st->curl_buffer_size;
        dl_st->mmap_id++;
        /*3.copy the left*/
        if (dl_st->package_offset > 0)
        {
            memcpy(dl_st->curl_buffer, p, dl_st->package_offset);
            dl_st->buffer_left -= dl_st->package_offset;
        }
    }
    return real_size;
#else
    FILE *ps = (FILE *)file;
    return fwrite(p, size_of, size, ps);
#endif
}

static long _curl_get_current_file_size(const char *file)
{
    struct stat st;
    int fd;

    fd = open(file, O_RDONLY);
    fstat(fd, &st);
    close(fd);
    return st.st_size;
}

static int __curl_validate_only_code(bool multiple, long code)
{
    if (HTTP_RSP_CODE_400 == code)
    {
        perror("http 400 related errors");
        return -1;
    }
    else if (HTTP_RSP_CODE_200 == code)
    {
        printf("the http related code works well %ld\n", code);
    }
    else
    {
        printf("the http related code %s - %ld\n", multiple ? "normal as well as 200 " : "error", code);
    }

    return 0;
}

static int _curl_validate_http_code(struct curl_user_hdl *user, CURL *curl)
{
    double has_download_len;
    long http_code;
    int res;

    res = curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &has_download_len);
    if (res != CURLE_OK)
    {
        perror("curl easy getinfo error");
        return -1;
    }
    printf("The has-download-len %lf\n", has_download_len);

    res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
    if (res != CURLE_OK)
    {
        perror("curl easy getinfo error");
        return -1;
    }

    return __curl_validate_only_code(user->curl_is_multiple, http_code);
}

static size_t _curl_read_file_data(void *p, size_t size_of, size_t size, FILE *ps)
{
    return fread(p, size_of, size, ps);
}

long int curl_get_url_file_size(const char *url)
{
    CURL *curl = NULL;
    double size = -1;
    CURLcode http_code;

    curl = curl_easy_init();
    if (!curl)
    {
        perror("curl easy init error");
        return -1;
    }

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HEADER, 1);
    curl_easy_setopt(curl, CURLOPT_NOBODY, 1);

    http_code = curl_easy_perform(curl);
    if (CURLE_OK != http_code)
    {
        perror("curl easy perform error");
        goto out;
    }

    curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &size);

out:
    curl_easy_cleanup(curl);
    return (long int)size;
}

static void _curl_init_multiple(long int size, char *range_a, char *range_b, long int *filesize_a, long int *filesize_b)
{
    long int size_a = size / 2;
    long int size_b = size / 2 + size % 2;

    snprintf(range_a, 64, "0-%ld", size_a - 1);
    snprintf(range_b, 64, "%ld-%ld", size_b, size - 1);

    /* print range of downloading file */
    printf("the range_a is %s\n", range_a);
    printf("the range_b is %s\n", range_b);
    *filesize_a = size_a - 1;
    *filesize_b = size - size_b - 1;
}

int _curl_download_file(const char *url, void *file, struct curl_user_hdl *cl_user, const char *range)
{
    CURL *curl = curl_easy_init();
    if (curl)
    {
#if USE_MMAP
        struct curl_download_st dl_st = 
        {
            .buffer_left = MMAP_BUFFER_SIZE,
            .package_offset = 0,
            .curl_buffer_size = MMAP_BUFFER_SIZE,
            .curl_buffer = malloc(MMAP_BUFFER_SIZE),
            .mmap_file = *(int *)file,
            .mmap_id = 0
        };
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &dl_st);
#else
        FILE **p = (FILE **)file;
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, *p);
#endif
        curl_easy_setopt(curl, CURLOPT_URL, url);
        curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _curl_write_file_data);
        curl_easy_setopt(curl, CURLOPT_MAX_RECV_SPEED_LARGE, (curl_off_t)cl_user->curl_limit_speed_kb * CURL_SPEED_1K_BYTE);

        /* single way for being timeout */
        if (!cl_user->curl_is_multiple)
            curl_easy_setopt(curl, CURLOPT_TIMEOUT, cl_user->curl_timeout);

        if (cl_user->curl_get_progress_bar)
            curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, cl_user->curl_get_progress_bar);
        curl_easy_setopt(curl, CURLOPT_XFERINFODATA, curl);

        /* set range if setting bool */
        if (cl_user->curl_is_multiple && range)
        {
            curl_easy_setopt(curl, CURLOPT_RANGE, range);
        }

        CURLcode result = curl_easy_perform(curl);
#if USE_MMAP
        _curl_write_file_data(NULL, 0, 0, &dl_st);
        free(dl_st.curl_buffer);
#endif
        if (CURLE_OK != result || -1 == _curl_validate_http_code(cl_user, curl))
            goto download;
    }
    curl_easy_cleanup(curl);
    return 0;

download:
    cl_user->curl_break_point = _curl_get_current_file_size(cl_user->curl_file_location);
    curl_easy_cleanup(curl);
    return -1;
}

static void *_range_a(void *p)
{
#if USE_MMAP
    int ra = 0;
#else
    FILE *ra = NULL;
#endif
    int res = -1;

    struct _ranges_download *ranges = (struct _ranges_download *)p;

    _curl_download_file_start(&ra, ranges->location, ranges->curl, "wb");

    res = _curl_download_file(ranges->url, &ra, ranges->curl, ranges->range);
    if (-1 == res)
    {
        perror("_curl download file error");
        goto out;
    }

out:
    _curl_download_file_stop(&ra);
    return NULL;
}

static void *_range_b(void *p)
{
#if USE_MMAP
    int rb = 0;
#else
    FILE *rb = NULL;
#endif
    int res = -1;

    struct _ranges_download *ranges = (struct _ranges_download *)p;

    _curl_download_file_start(&rb, ranges->location, ranges->curl, "wb");

    res = _curl_download_file(ranges->url, &rb, ranges->curl, ranges->range);
    if (-1 == res)
    {
        perror("_curl download file error");
        goto out;
    }

out:
    _curl_download_file_stop(&rb);
    return NULL;
}

int _curl_multiple_download_file(const char *url, struct curl_user_hdl *user, const char *range_a, const char *range_b, const long int filesize_a, const long int filesize_b)
{

    char tmp_location_a[512] = "";
    char tmp_location_b[512] = "";
    pthread_t pids[2];
    int i = 0;
    char pthreads_info[128] = "";
    long int num_pids;

    num_pids = sizeof(pids) / sizeof(pthread_t);

    /* get the start-time */
    struct timeval pids_t_start, pids_t_finish;

    gettimeofday(&pids_t_start, NULL);

    snprintf(tmp_location_a, sizeof(tmp_location_a), "%s.tmp.a", user->curl_file_location);
    snprintf(tmp_location_b, sizeof(tmp_location_b), "%s.tmp.b", user->curl_file_location);

    /* set structure for ranges */
    struct _ranges_download rd_a =
    {
        .url = url,
        .curl = user,
        .location = tmp_location_a,
        .range = range_a
    };
    rd_a.curl->file_size = filesize_a;

    struct _ranges_download rd_b =
    {
        .url = url,
        .curl = user,
        .location = tmp_location_b,
        .range = range_b
    };
    rd_b.curl->file_size = filesize_b;

    pthread_create(&pids[0], NULL, _range_a, (void *)&rd_a);
    pthread_create(&pids[1], NULL, _range_b, (void *)&rd_b);

    for (; i < num_pids; i++)
    {
        pthread_join(pids[i], NULL);
    }

    snprintf(pthreads_info, sizeof(pthreads_info), "used %ld pthreads handling tasks", num_pids);

    /* get the finish-time */
    gettimeofday(&pids_t_finish, NULL);
    user->curl_get_spent_time(pthreads_info, &pids_t_start, &pids_t_finish);
    return 0;
}

int _curl_single_download_file(const char *url, struct curl_user_hdl *user)
{
#if USE_MMAP
    int file = 0;
#else
    FILE *file = NULL;
#endif
    int res = -1;

    /* set time */
    struct timeval start, finish;

    gettimeofday(&start, NULL);

    _curl_download_file_start(&file, user->curl_file_location, user, "wb");
    res = _curl_download_file(url, &file, user, NULL);
    if (-1 == res)
    {
        perror("_curl download file error");
        goto download;
    }
    _curl_download_file_stop(&file);

    gettimeofday(&finish, NULL);
    if (user->curl_get_spent_time)
        user->curl_get_spent_time("single-pthread", &start, &finish);
    return 0;

download:
    _curl_download_file_stop(&file);
    return -1;
}

int curl_download_file(const char *url, struct curl_user_hdl *cl_user)
{
    long int size = -1;
    int res;

    size = curl_get_url_file_size(url);

    /* set range if setting multiple */
    if (cl_user->curl_is_multiple)
    {
        char range_a[64] = "";
        char range_b[64] = "";
        long int filesize_a = 0;
        long int filesize_b = 0;
        _curl_init_multiple(size, range_a, range_b, &filesize_a, &filesize_b);
        res = _curl_multiple_download_file(url, cl_user, range_a, range_b, filesize_a, filesize_b);
        if (-1 == res)
        {
            perror("multiple download error");
            return -1;
        }
    }
    else
    {
        res = _curl_single_download_file(url, cl_user);
        if (-1 == res)
        {
            perror("_curl single download error");
            return -1;
        }
    }
    return 0;
}

static void _curl_upload_file_start(FILE **p, struct curl_user_hdl *cl_user)
{
    *p = fopen(cl_user->curl_file_location, "wb");
    if (NULL == *p)
    {
        perror("open user curl_file_location error");
        return;
    }
}

static void _curl_upload_file_stop(FILE **p)
{
    if (*p)
        fclose(*p);
}

static int _curl_upload_file(const char *url, FILE **p, struct curl_user_hdl *cl_user)
{
    struct stat size_set;
    stat(cl_user->curl_file_location, &size_set);

    if (!size_set.st_size)
    {
        perror("size zero error");
        return -1;
    }

    CURL *curl = curl_easy_init();
    if (curl)
    {
        curl_easy_setopt(curl, CURLOPT_URL, url);
        curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
        curl_easy_setopt(curl, CURLOPT_READDATA, *p);
        curl_easy_setopt(curl, CURLOPT_READFUNCTION, _curl_read_file_data);
        curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
        curl_easy_setopt(curl, CURLOPT_MAX_SEND_SPEED_LARGE, (curl_off_t)cl_user->curl_limit_speed_kb * 10);
        curl_easy_setopt(curl, CURLOPT_XFERINFODATA, curl);
        curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)size_set.st_size);
        curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (curl_off_t)size_set.st_size);
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
        curl_easy_setopt(curl, CURLOPT_POST, 1L);
        curl_easy_setopt(curl, CURLOPT_TIMEOUT, cl_user->curl_timeout);
        curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
        curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);

        if (cl_user->curl_get_progress_bar)
            curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, cl_user->curl_get_progress_bar);

        curl_easy_setopt(curl, CURLOPT_XFERINFODATA, curl);

        /* set time */
        struct timeval start, finish;

        gettimeofday(&start, NULL);

        CURLcode result = curl_easy_perform(curl);
        if (CURLE_OK != result || -1 == _curl_validate_http_code(cl_user, curl))
            goto upload;

        gettimeofday(&finish, NULL);
        if (cl_user->curl_get_spent_time)
            cl_user->curl_get_spent_time("single-pthread", &start, &finish);
    }
    curl_easy_cleanup(curl);
    return 0;

upload:
    cl_user->curl_break_point = _curl_get_current_file_size(cl_user->curl_file_location);
    curl_easy_cleanup(curl);
    return -1;
}

int curl_upload_file(const char *url, struct curl_user_hdl *cl_user)
{
    FILE *upload = NULL;
    int res;

    _curl_upload_file_start(&upload, cl_user);

    res = _curl_upload_file(url, &upload, cl_user);
    if (-1 == res) {
        perror("_curl upload file error");
        goto err;
    }

err:
    _curl_upload_file_stop(&upload);
    return res;
}

static size_t _curl_header_write_data(void *content, size_t size_of, size_t size, void *extra)
{
    printf("the size %ld\n", size);
    return 0;
}

char *curl_get_md5_of_file(const char *url)
{
    CURL *curl = NULL;
    CURLcode http_code;
    static char md5[1024] = "";

    curl = curl_easy_init();
    if (!curl)
    {
        perror("curl easy init error");
        return NULL;
    }

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HEADER, 1);
    curl_easy_setopt(curl, CURLOPT_NOBODY, 1);

    /* make md5 return */
    curl_easy_setopt(curl, CURLOPT_HEADERDATA, &md5);
    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, _curl_header_write_data);

    http_code = curl_easy_perform(curl);
    if (CURLE_OK != http_code)
    {
        printf("curl easy perform error %d", http_code);
        goto out;
    }

    printf("the md5 of file %s\n", md5);

out:
    curl_easy_cleanup(curl);
    return md5;
}